[FaaS] Fn with Docker Hub #fnproject
こむろです。札幌寒い。
はじめに
前回Hello World Functionを作成しLocalサーバーでの実行を行いました。このままではローカルで立ち上げたサーバーで実行できるのみで、新たにサーバーを立ち上げてスケールさせることができません。
FunctionのDocker ImageをRepositoryにPushし、新たにサーバーを立ち上げても同じFunctionを実行できるような環境の構築を目指します。
今回はDocker RepositoryへのPushとImageのPullまでを確認します。
fnは発展途上のOSSです。今回紹介する内容は未来(次の日かも知れない)で正しくなくなる可能性があります。その点を踏まえた上で必要ならばご参照ください。
Docker Hub Repositoryを使ってDeployする
fnはdeployを実行すると、Functionを登録すると同時にFunctionのDocker Imageを作成し、RepositoryへのPushを行います。ImageのRegistryとしてDocker Hubがデフォルトで設定されています。今回はTutorialのCreating a Function from a Docker Imageに沿って作業を行います。そのためデフォルト設定のままImageをDocker HubへPushします。
環境の各種バージョン情報はこちら。
- Docker Hub Account
- fn version 0.5.15
- docker version 18.06.1-ce
What is Context
まずはfnのContext設定をチェックします。Contextとはfnコマンドを実行する上で設定されるClientの環境変数郡のようなもののようです。APIの呼び出し先を指定するAPI URL
やDocker Repositoryの設定の REGISTRY
といった必須項目が存在するのが確認できます。
Check Context
$ fn list context CURRENT NAME PROVIDER API URL REGISTRY * default default http://localhost:8999/v1
ここではCURRENT
に *
がついていることとAPI URL
のポート/パス及び REGISTRY
を確認します。fn invoke
でFunctionを実行する際には、このContextの情報をもとにfn-serverへのリクエストを作成します。
稀に先頭のチェックがついていない場合があり(Contextが指定されていない)、その場合は以下のコマンドでどのContextを利用するかを選択します。今回はdefault Contextを指定します。
$ fn use context default
再度チェックして CURRENT
に *
がついていればOKです。
Update Context
続いてREGISTRY
設定を更新します。fnはDocker HubをImageのRegistryとしてデフォルトで設定されており、特に何もしなければDocker HubへImageのPush, Pullを行うようになっています。Docker Hubの場合、REGISTRY
にはDocker Hubのログインユーザー名を登録します。
update
コマンドを利用し、Docker Hubのログインユーザー名を登録します。
$ fn update context registry hogeuser Current context updated registry with hogeuser
hogeuser
は適宜自分のアカウント等で差し替えてください。再度Contextを確認します。
$ fn list context CURRENT NAME PROVIDER API URL REGISTRY * default default http://localhost:8999/v1 hogeuser
REGISTRY
が設定されていればOKです。
Build and Deploy
準備ができましたので、FunctionのBuildとDeployを行っていきます。前回と同じくruntimeはgoで行きます。
まずは前回と同じくInitial Functionを作成します。
$ fn init --runtime go sample-docker
func.yaml
は以下のとおりです。
schema_version: 20180708 name: sample-docker version: 0.0.1 runtime: go entrypoint: ./func format: http-stream
生成したFunctionのImageを作成し、fn-serverへDeployします。
$ fn deploy --app sample-docker --no-bump Deploying sample-docker to app: sample-docker Building image hogeuser/sample-docker:0.0.6 ........ Parts: [hogeuser sample-docker:0.0.6] Pushing hogeuser/sample-docker:0.0.6 to docker registry...The push refers to repository [docker.io/hogeuser/sample-docker] dd51cf4366ae: Pushed 2a92e0de9abf: Pushed 97dedccb7128: Pushed c9e8b5c053a2: Pushed 0.0.6: digest: sha256:beb34cd4af72d01fxxxxxcfd901f25a8143aaaaaaa9ddb74abf9b9cf6723f838 size: 1156 Updating function sample-docker using image hogeuser/sample-docker:0.0.6...
前回の deploy
コマンドとは異なり --local
がありません。ImageはDocker Hubの指定したユーザーのRepositoryへとPushされました。
オプションの `--no-bump` はTagバージョンをインクリメントせずにImageの作成を行います。こちらのオプションが存在しない場合は、ImageのTagは `0.0.2` へインクリメントされてDocker HubへPushされます。
念の為Docker HubのRepositoryにPushされているかも確認してみましょう。
これでFnサーバーを新たに立ち上げてもFunctionを再度Deployする必要がなく、ImageをPullするだけで同じFunctionが実行可能です。
Execution
では準備が整いました。Functionを実行してみます。前回はImageのPushをスキップしているだけなので実行結果に大きな違いはありません。
$ fn invoke sample-docker sample-docker {"message":"Hello World"}
実行引数も問題ありません。
$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker {"message":"Hello KOMURO"}
サーバー側のログを確認してみると、Containerが立ち上がりFunctionが実行されているのが分かります。
time="2018-10-26T09:00:06Z" level=info msg="starting call" action="server.handleFnInvokeCall)-fm" app_id=01CT94RF05NG8G00GZJ0000001 container_id=01CTQSKG05NG8G00GZJ0000003 fnID=01CT94RF15NG8G00GZJ0000002 fn_id=01CT94RF15NG8G00GZJ0000002 id=01CTQSM97NNG8G00GZJ0000004
Problems
Docker ImageをRepositoryから取得する際にはDocker Pullが必要になります。fnの場合、Serverをスケールする必要がある場合は以下のような動作になるかと思います。
- fn-serverのコンテナが新たに立ち上がる
- Functionのメタデータやキューイングに接続
- Load Balancerに参加
- Functionの呼び出しに応じて必要なDocker ImageをPull
- ImageからContainerを立ち上げてFunctionを実行
FunctionをDeployする際に、その際に起動しているfn-server側にもImageが配置されているようです。
そこで試しにfn-server内のImageをわざと削除してみて、fn-serverがFunction Imageを持っていない状況でFunctionを呼び出した場合にどうなるかを試してみました。
DeployされたDocker Imageを削除
$ docker exec -it xxxxxx /bin/sh /app # docker images REPOSITORY TAG IMAGE ID CREATED SIZE hogeuser/sample-docker 0.0.6 bbbbbbbbbbb 2 minutes ago 15.6MB <none> <none> cccccccccccc 2 minutes ago 667MB /app # docker rmi bbbbbbbbbbb Untagged: hogeuser/sample-docker:0.0.6 Untagged: hogeuser/sample-docker@sha256:61ed378152dada83145d1d24c76b1a43b530ef92330ee45941dfee90423f2487 Deleted: sha256:1133bda39eea0acde61f2008b2c74080b7712cf2675f7be303725001006448ff Deleted: sha256:7eaf1b7c4b56a589f8c130e8f7f1477040a9f960bd5d692e2ec57bd8c86a9eda Deleted: sha256:266680a62b4dbea0e614f0abe2608d534ea83e04a24b208d820f0a014ced43c5 Deleted: sha256:d1f7aad52cda8ac95ceab034d4aea86b50fc4cb49f4e2716a972cc82b14769c9 Deleted: sha256:3d442263cf619f365f1c24094f15b82ab8c3d4840c483a7aafb695a81eae0163
先程と同じく実行してみます。
$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker {"message":"Failed to pull image 'hogeuser/sample-docker:0.0.6': pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'"} Fn: Error calling function: status 404 See 'fn <command> --help' for more information. Client version: 0.5.15
エラーが返ってきました。
サーバー側のログを確認
time="2018-10-26T10:41:39Z" level=info msg="Pulling image" app_id=01CT94RF05NG8G00GZJ0000001 cpus= fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username= time="2018-10-26T10:41:42Z" level=error msg="Failed to pull image" app_id=01CT94RF05NG8G00GZJ0000001 cpus= error="API error (404): pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'" fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username= time="2018-10-26T10:41:42Z" level=info msg="filtering error" app_id=01CT94RF05NG8G00GZJ0000001 cpus= error="No such container: 01CTQZE7MDNG8G00GZJ000000A" fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128
下記のログを見るに docker login
を実行せよとのことです。
error="API error (404): pull access denied for hogeuser/sample-docker, repository does not exist or may require 'docker login'"
しかしfn-serverの起動プロセスが走り始めてしまうとdocker login
を実行するタイミングがありません。
fn_id=01CT94RF15NG8G00GZJ0000002 format=http-stream id=01CTQZE7MDNG8G00GZJ000000A idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry= stack=PrepareCookie username=
認証情報がないためRegistryの情報やUsernameの情報が抜けているのも確認できます。これではPullできないのは当然です。うーん、どうすれば良いでしょうか??
docker loginでの認証
再度サーバーのログを確認してみるとこんなログが出ているのが確認できました。
time="2018-10-26T08:51:12Z" level=info msg="no docker auths from config files found (this is fine)" error="open /root/.dockercfg: no such file or directory"
ふむー。.dockercfg
ファイルが存在しないというログが少々気になります。 *1しかし、fn-serverのIssueを見てみても問題なさそう
これは関係なさそうなので別の設定が足りていない模様。
Docker versionを確認
Container内部のClient, Serverのバージョンを確認してみます。
/app # docker version Client: Version: 17.12.0-ce API version: 1.35 Go version: go1.9.2 Git commit: c97c6d6 Built: Wed Dec 27 20:05:38 2017 OS/Arch: linux/amd64 Server: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:29:02 2018 OS/Arch: linux/amd64 Experimental: true
む。Clientが微妙に古い。自分のローカルマシンを確認してみます。
$ docker version Client: Version: 18.06.1-ce API version: 1.38 Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:21:31 2018 OS/Arch: darwin/amd64 Experimental: false Server: Engine: Version: 18.06.1-ce API version: 1.38 (minimum version 1.12) Go version: go1.10.3 Git commit: e68fc7a Built: Tue Aug 21 17:29:02 2018 OS/Arch: linux/amd64 Experimental: true
微妙に差異がありますね。気になるところではありますがこれもあまり関係なさそう。ふーむ?
.docker/config.json
Dockerではdocker loginした認証情報を config.json
に保管するようです。 *2
fn-serverにおけるconfig.jsonは /root/.docker/config.json
に配置されます。
fn-server起動後に確認してみると /root/.docker/config.json
は作成されていません。試しにContainer内部でDocker Hubへログインしてみると以下のファイルが作成されました。
{ "auths": { "https://index.docker.io/v1/": { "auth": "xxxxxxxxxxxx" } }, "HttpHeaders": { "User-Agent": "Docker-Client/17.12.0-ce (linux)" } }
authはDocker Hubへのログイン情報である username:password をBase64で変換した値が記述されています。
サーバーが立ち上がっていてもこれがないということは、docker login
が実行されていないということ。とすると以下の手順が必要なようです。
- fn-serverを起動(Containerを作成して起動)
- Container内部で
docker login
を手動で実行(fn-serverと同じプロセスで?).docker/config.json
が作成される
手動での実行が厳しい。
docker loginのタイミング
Tutorialには docker login
を実行することが求められています。しかしどこでという指定がなさそうです。そして考えうるタイミングとして以下の3つが考えられます。
- Containerを起動しているホスト側でdocker loginを実行する
- 起動したfn-server Containerへ接続し、fn-serverとは別のプロセスでdocker loginを実行する
- fn-serverのプロセス内部でdocker loginを実行する
ホスト側でdocker login
こちらを実行してみましたが、そもそもホスト側はmacOS, Container側はLinuxなのであまり意味がなさそう・・・。当然ながらエラーは解消せず。
/bin/shのプロセスでdocker login
以下のコマンドでContainer内部へ /bin/sh
を通して新しいプロセスを作成しdocker loginを実行しました。 /root/.docker/config.json
が作成されましたがfn-server側のプロセスはこのファイルを認識してくれず。エラーは解消されません。
fn-serverのプロセス内部でdocker login
fn-serverのプロセスの起動の途中で docker loginを入れることは難しそうです。自分には処理をねじ込む方法が思いつきませんでした。断念。
docker loginを回避させる
docker login
を手動で実行するのは厳しそうです。そこでホスト側へDocker Hubのログイン情報のconfig.jsonを事前に準備しておき、Container起動時に規定のパスのファイルをホスト側のファイルでマウントする形で回避します。これならばホスト側でCredential情報を持っているため、fn-serverのContainerが立ち上がるたびにログイン作業を実施するという手間がなくなるはずです。
ホスト側のconfig.jsonをMountさせる
fn-serverの実体はDocker Imageから起動されるContainerです。そのため通常のContainerと同様に docker run
で起動させることができます。 fn start
ではContainer起動時の細かい制御オプションがなさそうなので、 docker run
で細かいオプションを自分で設定してfn-serverの起動を試みます。
実行コマンドは以下になります。
$ docker run --rm --privileged -t -i -v /Users/hogeuser/Develop/temp/config.json:/root/.docker/config.json -h fnserver -p 8999:8080 fnproject/fnserver
これで正常にfn-serverの起動ができました。重要そうな部分のオプションのみ下記に解説しておきます。
オプション | 説明 |
---|---|
--rm |
ContainerがStopした際に自動的にContainerを削除する設定です。起動オプションを試行する都合上、Containerが終了したらそのまま削除までやってほしいので指定します(別になくてもいい) |
--privileged |
マウント先のパスを見ると分かりますが、root配下のファイルを書き換える形になります。そのため、root権限での作業が必要になります。これを指定しない場合は Permission Denied によりContainerの起動に失敗し、fn-serverのプロセスが立ち上がりません(設定ファイルの配置の都合上必須) |
-v /Users/hogeuser/Develop/temp/config.json:/root/.docker/config.json |
ホスト側のファイルをContainer側の指定されたパスへマウントさせます(設定ファイルを書き換えないと意味がないので必須) |
-p 8999:8080 |
fn-serverのプロセスは8080で立ち上がりますが、前回までのcontextの設定上、8999番ポートを利用してAPIは実行されます。Container内部で8999を8080へルーティングしてもらいます。 |
上記コマンドでは `-d` のデタッチ指定をしていません。サーバーのログを見て正常に動いているかを確認したかったためです。特に問題がなければ `-d` を指定して起動としたほうが良いかと思います。ちなみにその際は `--rm` は同時に指定できないとのことなのでご注意ください。http://docs.docker.jp/engine/reference/run.html
Invoke
サーバーの準備ができたので、前回までと同じくFunctionの実行をしてみます。
$ echo -n '{"name":"KOMURO"}' | fn invoke sample-docker sample-docker {"message":"Hello KOMURO"}
正常に実行できました。前回まで出ていたエラーも出ていません。Docker Container内部のImageを見てみるとこの通り。
/app # docker images REPOSITORY TAG IMAGE ID CREATED SIZE hogeuser/sample-docker 0.0.6 91b93c79c029 6 minutes ago 15.6MB
正常にPullできているようです。サーバー側のログも確認します。
INFO[2018-10-28T12:06:24Z] Fn serving on `:8080` type=full INFO[2018-10-28T12:11:28Z] Pulling image app_id=01CTX9BXHENG8G00GZJ0000001 cpus= fn_id=01CTX9BXHQNG8G00GZJ0000002 format=http-stream id=01CTX9C3R4NG8G00GZJ0000005 idle_timeout=30 image="hogeuser/sample-docker:0.0.6" memory=128 registry="https://index.docker.io/v1/" stack=PrepareCookie username=hogeuser INFO[2018-10-28T12:11:38.353638300Z] ignoring event module=libcontainerd namespace=moby topic=/containers/create type="*events.ContainerCreate" INFO[0317] shim docker-containerd-shim started address="/containerd-shim/moby/372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da/shim.sock" debug=false module="containerd/tasks" pid=173 WARN[2018-10-28T12:11:38.682769200Z] unknown container container=372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da module=libcontainerd namespace=plugins.moby WARN[2018-10-28T12:11:38.704735200Z] unknown container container=372fd11d849596a71a2be4f8b96ea8d14c4dd565353516577513d83cb07d80da module=libcontainerd namespace=plugins.moby INFO[2018-10-28T12:11:38Z] starting call action="server.handleFnInvokeCall)-fm" app_id=01CTX9BXHENG8G00GZJ0000001 container_id=01CTX9C3R4NG8G00GZJ0000005 fnID=01CTX9BXHQNG8G00GZJ0000002 fn_id=01CTX9BXHQNG8G00GZJ0000002 id=01CTX9C3R2NG8G00GZJ0000003
unknown containerという WARN ログが出ているのが若干気になるものの、正常にPullできているようです。今まで表示されていなかった username
にも正常にユーザー名があるのが確認できます。
まとめ
Docker Hubを利用してRemote RepositoryへのImageのPush及びPullが行えることを確認しました。Tutorialは一通りうまくいくのですが、docker imageがない場合はどうなるんだろうと思って実行した結果、予想外の結果になったため調査してみました。結局のところ、fnコマンドでの実行ではなくdocker runで直接Containerを自分で立ち上げるという折角細かいオプションをうまくラップして隠蔽している裏側のコマンドを自分で叩くという解決方法になってしまい、これで良いのか?という気持ちです。
一応、回避策は見つけられたものの、fn-serverの動作をまだ完全に正しく理解しているわけではないため、オプションを見逃しているかもしれません。しかし、回避策によって新しくサーバーが立ち上がってImageない状況でもFunctionを呼び出すと自動的にImageをPullしてFunctionが実行されるという意図通りの動作が確認できました。おそらく今後も様々なアップデートで進化していくことでしょう。
まだまだ開発途中であるため、今回の事象はなんとか英語でまとめてIssueなりで報告しようかと。次は多分lbかLocal Repositoryあたりのネタを書こうかなと思います。
knative も気になるため、並行して追っていければ。
それではご機嫌やう。
参照
- fn tutorial - Creating a Function from a Docker Image
- Developers.IO - [FaaS] はじめてのfn #fnproject
- Change error message to info or warn on startup #301
- Docker run リファレンス
- 自分用 Docker Hub アカウント
- login
- fsouza/go-dockerclient